/*
 * Copyright 2011-2025 the original author or authors.
 *
 * Licensed under the Apache License, Version 2.0.
 * See `LICENSE` in the project root for license information.
 */

package me.ijleex.platform.framework.util;

import java.nio.charset.StandardCharsets;
import java.security.InvalidAlgorithmParameterException;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.SecureRandom;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.util.Base64;
import javax.crypto.BadPaddingException;
import javax.crypto.Cipher;
import javax.crypto.IllegalBlockSizeException;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.PBEKeySpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * AES（GCM）加密/解密工具.
 *
 * @author liym
 * @since 2024-08-09 09:26 新建
 */
public final class AESUtils {

    // com.sun.crypto.provider.GaloisCounterMode$AES256

    /**
     * 加密算法
     *
     * @see Lazy#CIPHER_XFORM_NAME
     */
    private static final String ALGORITHM = "AES";
    /**
     * 密钥生成算法
     */
    private static final String KEY_ALGORITHM = "PBKDF2WithHmacSHA256";
    /**
     * 派生密钥长度（128/192/256）
     */
    private static final int AES_KEY_SIZE = 256;
    /**
     * 初始向量长度
     *
     * <p>随机生成的初始向量，通常以明文的形式和密文一起传送。作用和MD5的“加盐”有些类似，目的是防止同样的明文块，始终加密成同样的密文块。</p>
     */
    private static final int GCM_IV_LENGTH = 16;
    /**
     * MAC标签长度（128/120/112/104/96）
     *
     * <p>MAC标签，用于确保数据在传输和存储过程中不会被意外更改或恶意篡改。该标签随后在解密操作期间使用，以确保密文和AAD未被篡改。在加密时，Mac Tag由明文、密钥Key、IV、AAD共同产生。</p>
     */
    private static final int GCM_TAG_LENGTH = 128;

    /**
     * PBKDF2函数迭代次数
     */
    private static final int PBKDF2_ITERATION_COUNT = 65535;

    private AESUtils() {
        throw new IllegalAccessError("Utility class");
    }

    /**
     * 加密
     *
     * @param plainTxt 明文，待加密的内容
     * @param password 密码，用于生成密钥；加密与解密必须使用同一个密码
     * @return 密文，加密结果
     * @see #decrypt(String, String) 解密
     * @see #genIV()
     * @see #genKey(String, byte[])
     * @since 2024-08-09 09:39
     */
    public static String encrypt(String plainTxt, String password) {
        byte[] plainBytes = plainTxt.getBytes(StandardCharsets.UTF_8);

        // 生成随机的IV内容
        byte[] iv = genIV();
        GCMParameterSpec paramSpec = new GCMParameterSpec(GCM_TAG_LENGTH, iv);

        byte[] cipherBytes;
        try {
            Cipher cipher = Lazy.CIPHER_THREAD_LOCAL.get();
            cipher.init(Cipher.ENCRYPT_MODE, genKey(password, iv), paramSpec);
            cipherBytes = cipher.doFinal(plainBytes);
        } catch (NoSuchAlgorithmException
                 | InvalidKeySpecException
                 | InvalidKeyException
                 | InvalidAlgorithmParameterException
                 | IllegalBlockSizeException
                 | BadPaddingException e) {
            throw new SecurityException("加密失败", e);
        }

        // 将IV拼接在密文的前面组合返回（用于解密时获取IV内容）
        byte[] resultBytes = new byte[cipherBytes.length + iv.length];
        System.arraycopy(iv, 0, resultBytes, 0, iv.length);
        System.arraycopy(cipherBytes, 0, resultBytes, iv.length, cipherBytes.length);

        return Base64.getEncoder().encodeToString(resultBytes);
    }

    /**
     * 解密
     *
     * @param cipherTxt 密文，待解密的内容
     * @param password 密码，用于生成密钥；加密与解密必须使用同一个密码
     * @return 明文，解密结果
     * @see #encrypt(String, String) 加密
     * @see #genIV()
     * @see #genKey(String, byte[])
     * @since 2024-08-09 10:17
     */
    public static String decrypt(String cipherTxt, String password) {
        byte[] inputBytes = Base64.getDecoder().decode(cipherTxt);

        // 从输入内容的前面提取分离IV内容
        GCMParameterSpec paramSpec = new GCMParameterSpec(GCM_TAG_LENGTH, inputBytes, 0, GCM_IV_LENGTH);
        byte[] iv = paramSpec.getIV();

        byte[] plainBytes;
        try {
            Cipher cipher = Lazy.CIPHER_THREAD_LOCAL.get();
            cipher.init(Cipher.DECRYPT_MODE, genKey(password, iv), paramSpec);
            plainBytes = cipher.doFinal(inputBytes, GCM_IV_LENGTH, inputBytes.length - GCM_IV_LENGTH);
        } catch (NoSuchAlgorithmException
                 | InvalidKeySpecException
                 | InvalidKeyException
                 | InvalidAlgorithmParameterException
                 | IllegalBlockSizeException
                 | BadPaddingException e) {
            throw new SecurityException("解密失败", e);
        }

        return new String(plainBytes, StandardCharsets.UTF_8);
    }

    /**
     * 生成初始向量
     *
     * @return IV
     * @see #GCM_IV_LENGTH
     */
    private static byte[] genIV() {
        byte[] bytes = new byte[GCM_IV_LENGTH];
        Lazy.SECURE_RANDOM.nextBytes(bytes);
        return bytes;
    }

    /**
     * 使用基于口令的密钥派生函数生成密钥
     *
     * @param password 口令
     * @param salt 盐，安全生成的随机字节
     * @return 密钥
     * @throws NoSuchAlgorithmException 加密算法不支持：{@value #KEY_ALGORITHM}
     * @throws InvalidKeySpecException 密钥规范不合法
     * @see #ALGORITHM
     * @see #KEY_ALGORITHM
     * @see #AES_KEY_SIZE
     * @see #PBKDF2_ITERATION_COUNT
     */
    private static SecretKey genKey(String password, byte[] salt)
            throws NoSuchAlgorithmException, InvalidKeySpecException {
        SecretKeyFactory factory = SecretKeyFactory.getInstance(KEY_ALGORITHM);
        KeySpec keySpec = new PBEKeySpec(password.toCharArray(), salt, PBKDF2_ITERATION_COUNT, AES_KEY_SIZE);
        return new SecretKeySpec(factory.generateSecret(keySpec).getEncoded(), ALGORITHM);
    }

    private static class Lazy {

        /**
         * 安全随机数生成工具
         */
        private static final SecureRandom SECURE_RANDOM = new SecureRandom();

        /**
         * 加密方式（加密算法/反馈模式/填充方案）
         */
        private static final String CIPHER_XFORM_NAME = "AES_256/GCM/NoPadding";

        /**
         * 加密处理（线程安全）
         */
        private static final ThreadLocal<Cipher> CIPHER_THREAD_LOCAL = createCipher();

        private Lazy() {
            throw new IllegalAccessError("No instances!");
        }

        /**
         * 初始化加密方式（线程安全）
         *
         * @see #CIPHER_XFORM_NAME
         */
        private static ThreadLocal<Cipher> createCipher() {
            return ThreadLocal.withInitial(() -> {
                Cipher cipher;
                try {
                    cipher = Cipher.getInstance(CIPHER_XFORM_NAME);
                } catch (NoSuchAlgorithmException | NoSuchPaddingException e) {
                    throw new SecurityException("加密初始化失败，加密方式不支持：" + CIPHER_XFORM_NAME, e);
                }
                return cipher;
            });
        }

    }

}
